Conway's Game of Life

Conway's Game of Life is a classic demonstration of emergence, where higher level patterns form from a few simple rules. Fantastic patterns emerge when the game is let to run long enough.

The rules here, to borrow from Wikipedia, are as follows:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

Below is a simple Conway's Game of Life implementation:


In [ ]:
import random

def new_board(x, y, num_live_cells=2, num_dead_cells=3):
    """Initializes a board for Conway's Game of Life"""
    board = []
    for i in range(0, y):
        # Defaults to a 3:2 dead cell:live cell ratio
        board.append([random.choice([0] * num_dead_cells + [1] * num_live_cells) for _ in range(0, x)])
    return board

        
def get(board, x, y):
    """Return the value at location (x, y) on a board, wrapping around if out-of-bounds"""
    return board[y % len(board)][x % len(board[0])]


def assign(board, x, y, value):
    """Assigns a value at location (x, y) on a board, wrapping around if out-of-bounds"""
    board[y % len(board)][x % len(board[0])] = value


def count_neighbors(board, x, y):
    """Counts the number of living neighbors a cell at (x, y) on a board has"""
    return sum([
        get(board, x - 1, y),
        get(board, x + 1, y),
        get(board, x, y - 1),
        get(board, x, y + 1),
        get(board, x + 1, y + 1),
        get(board, x + 1, y - 1),
        get(board, x - 1, y + 1),
        get(board, x - 1, y - 1)])


def process_life(board):
    """Creates the next iteration from a passed state of Conway's Game of Life"""
    next_board = new_board(len(board[0]), len(board))
    for y in range(0, len(board)):
        for x in range(0, len(board[y])):
            num_neighbors = count_neighbors(board, x, y)
            is_alive = get(board, x, y) == 1
            if num_neighbors < 2 and is_alive:
                assign(next_board, x, y, 0)
            elif 2 <= num_neighbors <= 3 and is_alive:
                assign(next_board, x, y, 1)
            elif num_neighbors > 3 and is_alive:
                assign(next_board, x, y, 0)
            elif num_neighbors == 3 and not is_alive:
                assign(next_board, x, y, 1)
            else:
                assign(next_board, x, y, 0)
    return next_board

A text-based example

To plot a simple version of Conway's Game of Life, we can use a print function:


In [ ]:
from IPython.display import clear_output
import time

def draw_board(board):
    res = ''
    for row in board:
        for col in row:
            if col == 1:
                res += '* '
            else:
                res += '  '
        res += '\n'
    return res

board = new_board(20, 20)

NUM_ITERATIONS = 100

for i in range(0, NUM_ITERATIONS):
    print('Iteration ' + str(i + 1))
    board = process_life(board)
    res = draw_board(board)
    print(res)
    time.sleep(0.1)
    clear_output(wait=True)

pydeck implementation

We can use either the PointCloudLayer or ScatterplotLayer from deck.gl to visualize the game.


In [ ]:
import numpy as np
import pandas as pd
import pydeck as deck

PINK = [155, 155, 255, 245]
PURPLE = [255, 155, 255, 245]

SCALING_FACTOR = 1000.0

def convert_board_to_df(board):
    """Makes the board matrix into a list for easier processing"""
    rows = []
    for x in range(0, len(board[0])):
        for y in range(0, len(board)):
            rows.append([[x / SCALING_FACTOR, y / SCALING_FACTOR], PURPLE if board[y][x] else PINK])
    return pd.DataFrame(rows, columns=['position', 'color'])

board = new_board(30, 30)
records = convert_board_to_df(board)
layer = deck.Layer(
    'PointCloudLayer',
    records,
    get_position='position',
    get_color='color',
    get_radius=40)
view_state = deck.ViewState(latitude=0.00, longitude=0.00, zoom=13, bearing=44, pitch=45)
r = deck.Deck(layers=[layer], initial_view_state=view_state, map_style='')
r.show()

To play the game over time, we call update in a loop.


In [ ]:
NUM_ITERATIONS = 100
display(r.show())
for i in range(0, NUM_ITERATIONS):
    board = process_life(board)
    records = convert_board_to_df(board)
    layer.data = records
    r.update()
    time.sleep(0.1)